knitr::opts_chunk$set(warning = FALSE, error = FALSE)
library(dplyr)
library(tidyr)
library(ggplot2)
library(readr)
library(DescTools)
library(sf)
library(tmap)
library(corrplot)
library(data.table)
library(purrr)
library(car)
library(Metrics)
library(rsq)
library(jtools)
library(gt)

Introduction

The aim of this analysis is to study the impact of recent migration to Vienna on the election results of the national election in 2024.

The analysis is carried out on a district level, studying the average yearly migration to each district and correlating it with percentage of votes for each party in the districts of Vienna.

The types of migration studied are: * Net external migration (ext_net): net migration to Vienna from abroad * Net internal migration (int_net): net migration to Vienna from Austria * Net local migration (local_net): net migration between Viennese districts * internal local migration (local_internal): total migration within Viennese districts

The source data is given in total numbers of persons migration, which for the sake of comparisons is recalculated as percentage of population living in a district.

For the analysis, a linear regression model is implemented for each party, with the election results for the given party as target variable.

The analysis is complemented with the sociodemographic variables of average age, percentage of females and income for each district for 2023 to control for their influence on voting pattern.

Data import

We start by importing the necessary data for the analysis. The election results have been sourced from https://www.wien.gv.at/wahlergebnis/de/NR241/index.html, the yearly migration from 2007 to 2023 per district with population and ge data has been cleaned in previous analysis and is accessed through the output_data folder. The social demographic indicators are sourced from https://www.data.gv.at/katalog/dataset/9ecf5866-dbe8-4cb2-b156-5097c7eec01f

Election data

We tidy up the election by removing non relevant columns and rows, renaming columns and turning the number of votes into a percentage of votes for each party.

head(election)
## # A tibble: 6 × 19
##   NUTS   Gebietsname     Wahlberechtigte Abgegebene `Ung?ltige` `G?ltige`  `?VP`
##   <chr>  <chr>                     <dbl>      <dbl>       <dbl>     <dbl>  <dbl>
## 1 G00000 ?sterreich              6346059    4929745       46857   4882888 1.28e6
## 2 G10000 Burgenland               233738     192899        2800    190099 5.43e4
## 3 G10099 Wahlkarten - B…               0         41          41         0 0     
## 4 G1A000 Burgenland Nord          124968     103415        1389    102026 2.80e4
## 5 G1A099 Wahlkarten - B…               0        303           2       301 4.9 e1
## 6 G1B000 Burgenland S?d           108770      89443        1370     88073 2.63e4
## # ℹ 12 more variables: `SP?` <dbl>, `FP?` <dbl>, `GR?NE` <dbl>, NEOS <dbl>,
## #   BIER <dbl>, MFG <dbl>, BGE <dbl>, LMP <dbl>, GAZA <dbl>, `KP?` <dbl>,
## #   KEINE <dbl>, ...19 <lgl>
election = election %>% filter(substr(NUTS, 1, 2) == "G9" &
                                 Wahlberechtigte > 0 &
                                 substr(NUTS, 5, 6) == "00")
election = election[9:nrow(election),]
election$bezirk = 1:23
election = subset(
  election,
  select = -c(
    NUTS,
    Gebietsname,
    Wahlberechtigte,
    Abgegebene,
    `Ung?ltige`,
    `G?ltige`,
    ...19,
    BGE
  )
)

colnames(election) = c(
  "ÖVP",
  "SPÖ",
  "FPÖ",
  "GRÜNE",
  "NEOS",
  'BIER',
  "MFG",
  "LMP",
  "GAZA",
  "KPÖ",
  "KEINE",
  "bezirk"
)

election$bezirk = as.factor(election$bezirk)

# turn election results into percentage
perc = function(vec) {
  vec = vec / sum(vec)
}

election = apply(election %>% select(!bezirk),
                 FUN = perc,
                 MARGIN = 1)

election = t(as.data.frame(election))
election = election * 100 
rownames(election) = 1:nrow(election)
election = as.data.frame(election)
election$bezirk = as.factor(1:nrow(election))
parliament = c("SPÖ", "ÖVP", "FPÖ", "GRÜNE", "NEOS")
election = election[,c(parliament, "bezirk")]

District data

The subdistricts are first summed up into district (Bezirk) results and then averaged over time. The dataframe is then split into two, one for austrian migration and one for foreign migration. Columns indicating net migration are kept.

#transforming yearly migration to percentage of current population of that year

year_bezirk_sex_nat = df %>% group_by(year, bezirk, sex, nationality) %>%
  summarize(
    net = sum(net),
    ext_net = sum(ext_net),
    int_net = sum(int_net),
    local_net = sum(local_net),
    local_internal = sum(local_internal),
    net_p = sum(net_p),
    ext_net_p = sum(ext_net_p),
    int_net_p = sum(int_net_p),
    local_net_p = sum(local_net_p),
    local_internal_p = sum(local_internal_p)
  )
## `summarise()` has grouped output by 'year', 'bezirk', 'sex'. You can override
## using the `.groups` argument.
#average yearly net migration from 2007 to 2023 per nationality
bezirk_nat = year_bezirk_sex_nat %>% group_by(bezirk, nationality) %>%
  summarize(
    net_p = mean(net_p),
    ext_net_p = mean(ext_net_p),
    int_net_p = mean(int_net_p),
    local_net_p = mean(local_net_p),
    local_internal_p = mean(local_internal_p)
  )
## `summarise()` has grouped output by 'bezirk'. You can override using the
## `.groups` argument.
bezirk_nat$bezirk = as.factor(bezirk_nat$bezirk)


bezirk_election = bezirk_nat %>% inner_join(election, by = c("bezirk" = "bezirk"))


tot_mig = as.data.frame(bezirk_election) %>% group_by(bezirk) %>% summarize(
  net_p = sum(net_p),
  ext_net_p = sum(ext_net_p),
  int_net_p = sum(int_net_p),
  local_net_p = sum(local_net_p),
  local_internal_p = sum(local_internal_p)
)

tot_mig = as.data.frame(tot_mig) %>%
  inner_join(election, by = c("bezirk" = "bezirk"))

aut_mig = as.data.frame(bezirk_election) %>% filter(nationality == "Austrian")
for_mig = as.data.frame(bezirk_election) %>% filter(nationality == "Foreign")

Inspection of the data

head(tot_mig)
##   bezirk     net_p ext_net_p int_net_p local_net_p local_internal_p      SPÖ
## 1      1 0.7985032 0.6827393 0.1157639  -0.7353247        0.2340946 19.11643
## 2      2 0.8893954 0.6366993 0.2526961  -0.4800800        0.4762326 33.18164
## 3      3 0.7664210 0.6294257 0.1369953  -0.3473606        0.3645906 30.26192
## 4      4 0.8808056 0.7239800 0.1568256  -0.6044410        0.3489154 28.64438
## 5      5 0.8657836 0.6187252 0.2470584  -0.8688434        0.4041142 33.62679
## 6      6 0.9808469 0.6691661 0.3116808  -0.8023453        0.3254532 31.50847
##        ÖVP      FPÖ    GRÜNE     NEOS
## 1 30.84212 14.58013 12.12839 18.99799
## 2 13.63152 15.00865 17.36472 11.81114
## 3 17.34168 14.69525 15.32406 14.32886
## 4 18.75809 11.66338 18.18615 15.70096
## 5 12.99768 14.04125 17.32529 11.93927
## 6 15.15447 10.96311 20.05021 14.03864
head(aut_mig)
##   bezirk nationality        net_p   ext_net_p  int_net_p local_net_p
## 1      1    Austrian -0.024108004 -0.09811059 0.07400258 -0.36584982
## 2      2    Austrian -0.008464146 -0.07044653 0.06198239 -0.08092135
## 3      3    Austrian  0.004116163 -0.05942286 0.06353902 -0.21055977
## 4      4    Austrian  0.082479568 -0.02774039 0.11021995 -0.34475735
## 5      5    Austrian  0.036213530 -0.08294072 0.11915425 -0.52071695
## 6      6    Austrian  0.220244103 -0.01550328 0.23574738 -0.52707642
##   local_internal_p      SPÖ      ÖVP      FPÖ    GRÜNE     NEOS
## 1        0.1445852 19.11643 30.84212 14.58013 12.12839 18.99799
## 2        0.2713470 33.18164 13.63152 15.00865 17.36472 11.81114
## 3        0.2226034 30.26192 17.34168 14.69525 15.32406 14.32886
## 4        0.2044984 28.64438 18.75809 11.66338 18.18615 15.70096
## 5        0.2043232 33.62679 12.99768 14.04125 17.32529 11.93927
## 6        0.2037307 31.50847 15.15447 10.96311 20.05021 14.03864
head(for_mig)
##   bezirk nationality     net_p ext_net_p  int_net_p local_net_p
## 1      1     Foreign 0.8226112 0.7808499 0.04176127  -0.3694749
## 2      2     Foreign 0.8978595 0.7071458 0.19071374  -0.3991587
## 3      3     Foreign 0.7623048 0.6888485 0.07345633  -0.1368008
## 4      4     Foreign 0.7983260 0.7517204 0.04660564  -0.2596837
## 5      5     Foreign 0.8295701 0.7016659 0.12790416  -0.3481265
## 6      6     Foreign 0.7606028 0.6846694 0.07593338  -0.2752688
##   local_internal_p      SPÖ      ÖVP      FPÖ    GRÜNE     NEOS
## 1       0.08950944 19.11643 30.84212 14.58013 12.12839 18.99799
## 2       0.20488554 33.18164 13.63152 15.00865 17.36472 11.81114
## 3       0.14198714 30.26192 17.34168 14.69525 15.32406 14.32886
## 4       0.14441701 28.64438 18.75809 11.66338 18.18615 15.70096
## 5       0.19979098 33.62679 12.99768 14.04125 17.32529 11.93927
## 6       0.12172245 31.50847 15.15447 10.96311 20.05021 14.03864

Correlation analysis

As an example for correlations, we use the perhaps most interesting example in our context, migration vs the right populist anti-migration party FPÖ.

cor(x = aut_mig$net_p,
         y = aut_mig$FPÖ)
## [1] -0.8807193
mdl_1 = lm(data = aut_mig, FPÖ ~ net_p)
plot(x = aut_mig$net_p, y = aut_mig$FPÖ, ylab = "FPÖ election results (%)", xlab = "net migration of Austrian citizens (%)", main = "Net migration of Austrians vs FPÖ election results per bezirk") + 
  abline(a = mdl_1$coefficients[1], b = mdl_1$coefficients[2])

## integer(0)
cor(for_mig$net_p, for_mig$FPÖ)
## [1] -0.6007031
mdl_2 = lm(data = for_mig, FPÖ ~ net_p)
plot(x = for_mig$net_p, y = for_mig$FPÖ, ylab = "FPÖ election results (%)", xlab = "net migration of foreign citizens (%)", main = "Net migration of foreign citizens vs FPÖ election results per bezirk") + 
  abline(a = mdl_2$coefficients[1], b = mdl_2$coefficients[2])

## integer(0)

As can be seen, there is a very strong negative correlation between migration and FPÖ election results. Bezirks that have received relatively large amounts of migration, foreign and Austrian, tends to note vote for the FPÖ. Districts with relatively smaller amounts of migration instead have a strong tendency to vote for FPÖ. Viewing only the Austrian migration, the tendency is stronger (r = -0.88, p < 0.001); districts with a negative net migration of austrians strongly tend to vote FPÖ.

Correlation plot

cors_tot = as.data.frame(tot_mig) %>% select(!c(bezirk))
cm = cor(cors_tot)
corrplot(cm, title = "total migration")

cors_aut = as.data.frame(aut_mig) %>% select(!c(bezirk, nationality))
cm = cor(cors_aut)
corrplot(cm, title = "Austrian citizens")

cors_for = as.data.frame(for_mig) %>% select(!c(bezirk, nationality))
cm = cor(cors_for)
corrplot(cm, title = "foreign citizens")

From the correlation plots, it can be seen that the migration variables are somewhat intercorrelated, and show correlation with FPÖ, GRÜNE and to some extent NEOS. ÖVP and SPÖ on the other hand show no correlation with different types, but they are inversely correlated with each other.

get_correlations = function(data, covar, parties) {
  #takes a dataframe with data of party election results and a correlated variable, and returns a dataframe
  #with the correlation coefficient between the correlated variable and the election result of each party
  #with a 95% CI
  r = c()
  upper_ci = c()
  lower_ci = c()
  covar = data[, covar]
  for (party in parties) {
    part_results = data[, party]
    correlation = cor.test(covar, part_results)
    r = c(r, correlation$estimate)
    upper_ci = c(upper_ci, correlation$conf.int[1])
    lower_ci = c(lower_ci, correlation$conf.int[2])
  }
  df_out = data.frame(parties, r, upper_ci, lower_ci)
  return(df_out)
}

Correlation coefficients

To get a better look at the relationship, the correlation coefficients are calculated for the different variables and plotted as barcharts with 95% confident intervalls.

Net migration

corrs_tot = get_correlations(tot_mig, "net_p", parliament)

ggplot(data = corrs_tot, aes (y = r, x = parties, fill = parties)) +
  geom_bar(stat = 'identity', fill = c("red", "black", "blue", "green", "pink")) +
  geom_errorbar(aes(ymin = lower_ci, ymax = upper_ci), width = 0.1) +
  labs(title = "Corrrelation coefficients with 95% CI for voting patterns and migration (average yearly percent)", y = "Pearson's r")

corrs_aut = get_correlations(aut_mig, "net_p", parliament)

ggplot(data = corrs_aut, aes (y = r, x = parties, fill = parties)) +
  geom_bar(stat = 'identity', fill = c("red", "black", "blue", "green", "pink")) +
  geom_errorbar(aes(ymin = lower_ci, ymax = upper_ci), width = 0.1) +
  labs(title = "Corrrelation coefficients with 95% CI for voting patterns and austrian migration to Vienna (average yearly percent)", y = "Pearson's r")

corrs_for = get_correlations(for_mig, "net_p", parliament)

ggplot(data = corrs_for, aes (y = r, x = parties, fill = parties)) +
  geom_bar(stat = 'identity', fill = c("red", "black", "blue", "green", "pink")) +
  geom_errorbar(aes(ymin = lower_ci, ymax = upper_ci), width = 0.1) +
  labs(title = "Corrrelation coefficients with 95% CI for voting patterns and foreign migration (average yearly percent)", y = "Pearson's r")

As can be seen, districts with large amounts of migration relative to their population tends to vote for the green and NEOS, and not vote for FPÖ, for SPÖ and ÖVP no relation is found. The relationship is the strongest for austrian migration.

Within Vienna

corrs_aut = get_correlations(aut_mig, "local_net_p", parliament)

ggplot(data = corrs_aut, aes (y = r, x = parties, fill = parties)) +
  geom_bar(stat = 'identity', fill = c("red", "black", "blue", "green", "pink")) +
  geom_errorbar(aes(ymin = lower_ci, ymax = upper_ci), width = 0.1) +
  labs(title = "Corrrelation coefficients with 95% CI 2024 election results and austrian migration within Vienna (average yearly percent)", y = "Pearson's r")

corrs_for = get_correlations(for_mig, "local_net_p", parliament)

ggplot(data = corrs_for, aes (y = r, x = parties, fill = parties)) +
  geom_bar(stat = 'identity', fill = c("red", "black", "blue", "green", "pink")) +
  geom_errorbar(aes(ymin = lower_ci, ymax = upper_ci), width = 0.1) +
  labs(title = "Corrrelation coefficients with 95% CI for voting patterns and foreign migration (average yearly percent)", y = "Pearson's r")

corrs_tot = get_correlations(tot_mig, "local_net_p", parliament)
ggplot(data = corrs_tot, aes (y = r, x = parties, fill = parties)) +
  geom_bar(stat = 'identity', fill = c("red", "black", "blue", "green", "pink")) +
  geom_errorbar(aes(ymin = lower_ci, ymax = upper_ci), width = 0.1) +
  labs(title = "Corrrelation coefficients with 95% CI for voting patterns and migration (average yearly net number of migrants", y = "Pearson's r")

Looking at the within Vienna migration, the relationship is the opposite: districts with larger amount of migration tend to vote FPÖ and not GRÜNE and NEOS and again, there seems to be no relationship between migration and ÖVP and SPÖ.

Modelling

As the independent variables are intercorrelated, it is necessary to build a larger model to view them together, and also to add sociodemographic factors to control for and isolate the impact of migration.

Add Sociedemographics

## add socioeconomic indicator

#average income 2021 https://www.data.gv.at/katalog/dataset/d76c0e8b-c599-4700-8a88-29d0d87e563d

income = income %>%
  filter(REF_YEAR == 2021) %>%
  select(DISTRICT_CODE, INC_TOT_VALUE)
colnames(income) = c("bezirk", "income")
income = income[2:23, ]
income$bezirk = as.factor(as.numeric(substr(income$bezirk, 2, 3)))


## add demographic indicators https://www.data.gv.at/katalog/dataset/9ecf5866-dbe8-4cb2-b156-5097c7eec01f

age = age %>% filter(REF_YEAR == 2024) %>% select(DISTRICT_CODE, AGE_AVE)
colnames(age) = c("bezirk", "age")
age = age[2:23, ]
age$age = age$age / 100
age$bezirk = as.factor(as.numeric(substr(age$bezirk, 2, 3)))



sex = sex %>% filter(REF_YEAR == 2024) %>% mutate(perc_fem = POP_FEM / POP_TOTAL * 100) %>% select(DISTRICT_CODE, perc_fem)
colnames(sex) = c("bezirk", "perc_fem")
sex$bezirk = as.factor(as.numeric(substr(sex$bezirk, 2, 3)))

join dataframes to create model

aut_mig_var = as.data.frame(aut_mig) %>%
  select(bezirk, net_p, local_net_p, local_internal_p) %>%
  rename_with( ~ paste0(., "_aut"), c(net_p, local_net_p, local_internal_p))

for_mig_var = as.data.frame(for_mig) %>%
  select(bezirk, net_p, local_net_p, local_internal_p) %>%
  rename_with( ~ paste0(., "_for"), c(net_p, local_net_p, local_internal_p))

list_df = list(age, sex, income, aut_mig_var, for_mig_var, election[c(parliament, "bezirk")])

mdl_df = reduce(list_df, ~ inner_join(.x, .y, by = "bezirk"))

mdl_df = as.data.frame(mdl_df %>% select(!bezirk))

correlation plots

cm = cor(mdl_df)
corrplot(cm, method = "pie")

The demographic variables are intercorrelated, the migration variables as well, with within district migration of foreigners being correlated with demographic factors as well as ÖVP and SPÖ. ÖVP and SPÖ are correlated with demographic factors while FPÖ, GRÜNE and NEOS are correlated with migration variables.

FPÖ

df_mdl_FPÖ = subset(mdl_df, select = -c(SPÖ, ÖVP, GRÜNE, NEOS))

mdl_FPÖ = lm(data = df_mdl_FPÖ, FPÖ  ~ .)
summary(mdl_FPÖ)
## 
## Call:
## lm(formula = FPÖ ~ ., data = df_mdl_FPÖ)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -2.51660 -0.84668  0.04205  1.20960  1.89123 
## 
## Coefficients:
##                       Estimate Std. Error t value Pr(>|t|)   
## (Intercept)           82.65137   49.18274   1.680   0.1187   
## age                   -0.58091    0.75105  -0.773   0.4542   
## perc_fem              -1.06988    0.81114  -1.319   0.2118   
## income                 0.07364    0.41773   0.176   0.8630   
## net_p_aut            -29.93666    9.36994  -3.195   0.0077 **
## local_net_p_aut       -3.29468    5.10949  -0.645   0.5312   
## local_internal_p_aut  21.08788   24.18080   0.872   0.4003   
## net_p_for             18.56827    7.04681   2.635   0.0218 * 
## local_net_p_for       14.80330    5.39165   2.746   0.0177 * 
## local_internal_p_for -38.84309   30.68082  -1.266   0.2295   
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.843 on 12 degrees of freedom
## Multiple R-squared:  0.9533, Adjusted R-squared:  0.9184 
## F-statistic: 27.25 on 9 and 12 DF,  p-value: 1.21e-06
rsq(mdl_FPÖ)
## [1] 0.9533451
AIC(mdl_FPÖ)
## [1] 97.98824
RMSE(mdl_FPÖ)
## [1] 1.360792
vif(mdl_FPÖ) 
##                  age             perc_fem               income 
##            11.856388             6.238994            16.930084 
##            net_p_aut      local_net_p_aut local_internal_p_aut 
##            13.638026            15.032455             9.393337 
##            net_p_for      local_net_p_for local_internal_p_for 
##            14.428220            17.739515            10.131135
res = resid(mdl_FPÖ)
qqnorm(res)
qqline(res) # normally distributed residuals

mdl_FPÖ_scale = lm(data = as.data.frame(apply(
  df_mdl_FPÖ, MARGIN = 2, FUN = scale
)), FPÖ  ~ .)
summary(mdl_FPÖ_scale)
## 
## Call:
## lm(formula = FPÖ ~ ., data = as.data.frame(apply(df_mdl_FPÖ, 
##     MARGIN = 2, FUN = scale)))
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.39027 -0.13130  0.00652  0.18759  0.29329 
## 
## Coefficients:
##                        Estimate Std. Error t value Pr(>|t|)   
## (Intercept)          -5.838e-16  6.092e-02   0.000   1.0000   
## age                  -1.661e-01  2.147e-01  -0.773   0.4542   
## perc_fem             -2.054e-01  1.557e-01  -1.319   0.2118   
## income                4.523e-02  2.566e-01   0.176   0.8630   
## net_p_aut            -7.357e-01  2.303e-01  -3.195   0.0077 **
## local_net_p_aut      -1.559e-01  2.418e-01  -0.645   0.5312   
## local_internal_p_aut  1.667e-01  1.911e-01   0.872   0.4003   
## net_p_for             6.241e-01  2.368e-01   2.635   0.0218 * 
## local_net_p_for       7.211e-01  2.626e-01   2.746   0.0177 * 
## local_internal_p_for -2.513e-01  1.985e-01  -1.266   0.2295   
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.2857 on 12 degrees of freedom
## Multiple R-squared:  0.9533, Adjusted R-squared:  0.9184 
## F-statistic: 27.25 on 9 and 12 DF,  p-value: 1.21e-06
df_mdl_FPÖ_2 = subset(df_mdl_FPÖ,
                      select = -c(local_net_p_aut, age, local_internal_p_for))

mdl_FPÖ_2 = lm(data = df_mdl_FPÖ_2, FPÖ ~ .)
summary(mdl_FPÖ_2)
## 
## Call:
## lm(formula = FPÖ ~ ., data = df_mdl_FPÖ_2)
## 
## Residuals:
##    Min     1Q Median     3Q    Max 
## -2.662 -1.271  0.254  1.164  2.570 
## 
## Coefficients:
##                       Estimate Std. Error t value Pr(>|t|)    
## (Intercept)           35.94278   32.45430   1.107  0.28553    
## perc_fem              -0.56284    0.71629  -0.786  0.44424    
## income                -0.07536    0.20014  -0.377  0.71178    
## net_p_aut            -19.58364    5.02947  -3.894  0.00144 ** 
## local_internal_p_aut   3.89830   13.43561   0.290  0.77568    
## net_p_for             19.12883    5.06913   3.774  0.00184 ** 
## local_net_p_for       20.21386    3.77834   5.350  8.1e-05 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.823 on 15 degrees of freedom
## Multiple R-squared:  0.9429, Adjusted R-squared:  0.9201 
## F-statistic:  41.3 on 6 and 15 DF,  p-value: 1.716e-08
rsq(mdl_FPÖ_2) 
## [1] 0.942925
RMSE(mdl_FPÖ_2) 
## [1] 1.505101
AIC(mdl_FPÖ_2) 
## [1] 96.42314
vif(mdl_FPÖ_2)
##             perc_fem               income            net_p_aut 
##             4.971202             3.970886             4.014986 
## local_internal_p_aut            net_p_for      local_net_p_for 
##             2.963162             7.628822             8.901488
res = resid(mdl_FPÖ_2)
qqnorm(res)
qqline(res)

shapiro.test(res) #no significant support for deviation from normality
## 
##  Shapiro-Wilk normality test
## 
## data:  res
## W = 0.96679, p-value = 0.6369
mdl_FPÖ_2_scale = lm(data = as.data.frame(apply(
  df_mdl_FPÖ_2, MARGIN = 2, FUN = scale
)), FPÖ  ~ .)
summary(mdl_FPÖ_2_scale)
## 
## Call:
## lm(formula = FPÖ ~ ., data = as.data.frame(apply(df_mdl_FPÖ_2, 
##     MARGIN = 2, FUN = scale)))
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.41282 -0.19704  0.03938  0.18054  0.39848 
## 
## Coefficients:
##                        Estimate Std. Error t value Pr(>|t|)    
## (Intercept)          -1.907e-16  6.027e-02   0.000  1.00000    
## perc_fem             -1.081e-01  1.375e-01  -0.786  0.44424    
## income               -4.629e-02  1.229e-01  -0.377  0.71178    
## net_p_aut            -4.813e-01  1.236e-01  -3.894  0.00144 ** 
## local_internal_p_aut  3.081e-02  1.062e-01   0.290  0.77568    
## net_p_for             6.429e-01  1.704e-01   3.774  0.00184 ** 
## local_net_p_for       9.846e-01  1.840e-01   5.350  8.1e-05 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.2827 on 15 degrees of freedom
## Multiple R-squared:  0.9429, Adjusted R-squared:  0.9201 
## F-statistic:  41.3 on 6 and 15 DF,  p-value: 1.716e-08
plot_coefs(mdl_FPÖ_2_scale, colors = "darkblue") + theme_apa() + ggtitle("Beta coefficients for linear regression model, FPÖ results as dependent variable")
## Registered S3 methods overwritten by 'broom':
##   method            from  
##   tidy.glht         jtools
##   tidy.summary.glht jtools
## Loading required namespace: broom.mixed

best_mdl_FPÖ = mdl_FPÖ_2

When accounting for other factors such as income and gender, migration remains significant predictors of FPÖ election results. Specifically, an increase in Austrian migration is associated with worse results for FPÖ, and an increase in foreign migration both within and from outside Vienna is associated with increased support for FPÖ. The most important variable for FPÖ support is given by looking at which district most foreign citizens already living in Vienna are moving to.

GRÜNE

df_mdl_GRN = subset(mdl_df, select = -c(SPÖ, ÖVP, FPÖ, NEOS))

mdl_GRN = lm(data = df_mdl_GRN, GRÜNE ~ .)
summary(mdl_GRN)
## 
## Call:
## lm(formula = GRÜNE ~ ., data = df_mdl_GRN)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -1.5573 -0.8215 -0.3359  0.8486  2.1401 
## 
## Coefficients:
##                        Estimate Std. Error t value Pr(>|t|)    
## (Intercept)           52.069941  36.128475   1.441 0.175098    
## age                   -0.492724   0.551702  -0.893 0.389364    
## perc_fem               0.002438   0.595847   0.004 0.996802    
## income                -0.023943   0.306851  -0.078 0.939091    
## net_p_aut             23.207380   6.882937   3.372 0.005552 ** 
## local_net_p_aut        2.672406   3.753313   0.712 0.490069    
## local_internal_p_aut  -9.556891  17.762641  -0.538 0.600391    
## net_p_for            -24.237004   5.176418  -4.682 0.000530 ***
## local_net_p_for      -18.686354   3.960580  -4.718 0.000499 ***
## local_internal_p_for   9.763975  22.537400   0.433 0.672530    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.353 on 12 degrees of freedom
## Multiple R-squared:  0.9526, Adjusted R-squared:  0.9171 
## F-statistic: 26.82 on 9 and 12 DF,  p-value: 1.321e-06
rsq(mdl_GRN) 
## [1] 0.9526432
AIC(mdl_GRN) 
## [1] 84.41593
RMSE(mdl_GRN)
## [1] 0.9996054
vif(mdl_GRN)
##                  age             perc_fem               income 
##            11.856388             6.238994            16.930084 
##            net_p_aut      local_net_p_aut local_internal_p_aut 
##            13.638026            15.032455             9.393337 
##            net_p_for      local_net_p_for local_internal_p_for 
##            14.428220            17.739515            10.131135
res = resid(mdl_GRN)
qqnorm(res)
qqline(res)

shapiro.test(res) # no support for deviation from normality
## 
##  Shapiro-Wilk normality test
## 
## data:  res
## W = 0.93514, p-value = 0.157
df_mdl_GRN_2 = subset(
  df_mdl_GRN,
  select = -c(
    local_internal_p_aut,
    local_internal_p_for,
    local_net_p_aut,
    age
  )
)

mdl_GRN_2 = lm(data = df_mdl_GRN_2, GRÜNE ~ .)
summary(mdl_GRN_2)
## 
## Call:
## lm(formula = GRÜNE ~ ., data = df_mdl_GRN_2)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -1.0935 -0.9878 -0.3694  0.8841  2.0470 
## 
## Coefficients:
##                 Estimate Std. Error t value Pr(>|t|)    
## (Intercept)      24.4123    20.2417   1.206   0.2453    
## perc_fem          0.2138     0.4172   0.513   0.6153    
## income           -0.2943     0.1203  -2.446   0.0264 *  
## net_p_aut        21.5071     3.3563   6.408 8.67e-06 ***
## net_p_for       -20.6735     3.4508  -5.991 1.88e-05 ***
## local_net_p_for -16.3672     2.5502  -6.418 8.51e-06 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.245 on 16 degrees of freedom
## Multiple R-squared:  0.9465, Adjusted R-squared:  0.9298 
## F-statistic: 56.66 on 5 and 16 DF,  p-value: 1.311e-09
rsq(mdl_GRN_2)
## [1] 0.9465394
AIC(mdl_GRN_2) 
## [1] 79.0831
RMSE(mdl_GRN) 
## [1] 0.9996054
vif(mdl_GRN_2) 
##        perc_fem          income       net_p_aut       net_p_for local_net_p_for 
##        3.612733        3.075519        3.830217        7.573179        8.687109
res = resid(mdl_GRN_2)
qqnorm(res)
qqline(res)

shapiro.test(res)
## 
##  Shapiro-Wilk normality test
## 
## data:  res
## W = 0.85651, p-value = 0.004435
mdl_GRN_2_scale = lm(data = as.data.frame(scale(df_mdl_GRN_2)), GRÜNE ~ .)
summary(mdl_GRN_2_scale)
## 
## Call:
## lm(formula = GRÜNE ~ ., data = as.data.frame(scale(df_mdl_GRN_2)))
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.23259 -0.21009 -0.07856  0.18805  0.43539 
## 
## Coefficients:
##                   Estimate Std. Error t value Pr(>|t|)    
## (Intercept)     -7.546e-16  5.647e-02   0.000   1.0000    
## perc_fem         5.632e-02  1.099e-01   0.513   0.6153    
## income          -2.479e-01  1.014e-01  -2.446   0.0264 *  
## net_p_aut        7.249e-01  1.131e-01   6.408 8.67e-06 ***
## net_p_for       -9.530e-01  1.591e-01  -5.991 1.88e-05 ***
## local_net_p_for -1.093e+00  1.704e-01  -6.418 8.51e-06 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.2649 on 16 degrees of freedom
## Multiple R-squared:  0.9465, Adjusted R-squared:  0.9298 
## F-statistic: 56.66 on 5 and 16 DF,  p-value: 1.311e-09
plot_coefs(mdl_GRN_2_scale, colors = "green") + theme_apa() + ggtitle("Beta coefficients for linear regression model, GRÜNE results as dependent variable")
## Loading required namespace: broom.mixed

best_mdl_GRN = mdl_GRN_2

The model of GRÜNE results is a somewhat better fit than the one for FPÖ, reveealing an inverse of the relationship between migration and election results as observed for FPÖ.

A difference observed is that income has a significant effect on the support for GRÜNE, where higher income is associated with lower support. The effect is however small.

ÖVP

df_mdl_ÖVP = subset(mdl_df, select = -c(SPÖ, GRÜNE, FPÖ, NEOS))

mdl_ÖVP = lm(data = df_mdl_ÖVP, ÖVP ~ .)
summary(mdl_ÖVP)
## 
## Call:
## lm(formula = ÖVP ~ ., data = df_mdl_ÖVP)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1.47473 -0.51261 -0.04775  0.36151  1.98023 
## 
## Coefficients:
##                       Estimate Std. Error t value Pr(>|t|)    
## (Intercept)          -149.8813    28.5492  -5.250 0.000204 ***
## age                     2.0827     0.4360   4.777 0.000451 ***
## perc_fem                1.2903     0.4708   2.740 0.017920 *  
## income                  0.1518     0.2425   0.626 0.543026    
## net_p_aut             -11.5059     5.4390  -2.115 0.055986 .  
## local_net_p_aut        -0.6927     2.9659  -0.234 0.819271    
## local_internal_p_aut    7.0329    14.0363   0.501 0.625397    
## net_p_for              11.7382     4.0905   2.870 0.014099 *  
## local_net_p_for         5.8481     3.1297   1.869 0.086278 .  
## local_internal_p_for    6.8678    17.8094   0.386 0.706519    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.07 on 12 degrees of freedom
## Multiple R-squared:  0.9714, Adjusted R-squared:  0.9499 
## F-statistic: 45.24 on 9 and 12 DF,  p-value: 6.829e-08
rsq(mdl_ÖVP) 
## [1] 0.9713723
AIC(mdl_ÖVP) 
## [1] 74.05605
RMSE(mdl_ÖVP) 
## [1] 0.789902
vif(mdl_ÖVP) 
##                  age             perc_fem               income 
##            11.856388             6.238994            16.930084 
##            net_p_aut      local_net_p_aut local_internal_p_aut 
##            13.638026            15.032455             9.393337 
##            net_p_for      local_net_p_for local_internal_p_for 
##            14.428220            17.739515            10.131135
res = resid(mdl_ÖVP)
qqnorm(res)
qqline(res)

shapiro.test(res) # no support for deviation from normality
## 
##  Shapiro-Wilk normality test
## 
## data:  res
## W = 0.96156, p-value = 0.5214
df_mdl_ÖVP_2 = subset(
  df_mdl_ÖVP,
  select = -c(
    income,
    local_internal_p_aut,
    local_internal_p_aut,
    local_internal_p_for,
    local_net_p_aut
  )
)

mdl_ÖVP_2 = lm(data = df_mdl_ÖVP_2, ÖVP ~ .)
summary(mdl_ÖVP_2)
## 
## Call:
## lm(formula = ÖVP ~ ., data = df_mdl_ÖVP_2)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1.83169 -0.49195 -0.01817  0.30818  2.16547 
## 
## Coefficients:
##                  Estimate Std. Error t value Pr(>|t|)    
## (Intercept)     -159.2839    12.8132 -12.431 1.23e-09 ***
## age                2.1517     0.1667  12.908 7.10e-10 ***
## perc_fem           1.5433     0.2632   5.863 2.40e-05 ***
## net_p_aut        -12.2462     2.7430  -4.464 0.000391 ***
## net_p_for         12.0547     2.9213   4.127 0.000791 ***
## local_net_p_for    5.4384     2.2411   2.427 0.027420 *  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.006 on 16 degrees of freedom
## Multiple R-squared:  0.9662, Adjusted R-squared:  0.9557 
## F-statistic: 91.53 on 5 and 16 DF,  p-value: 3.424e-11
rsq(mdl_ÖVP_2) 
## [1] 0.966221
AIC(mdl_ÖVP_2) 
## [1] 69.69633
RMSE(mdl_ÖVP_2) 
## [1] 0.8580328
vif(mdl_ÖVP_2) 
##             age        perc_fem       net_p_aut       net_p_for local_net_p_for 
##        1.958857        2.203405        3.919690        8.315345       10.278273
res = resid(mdl_ÖVP_2)
qqnorm(res)
qqline(res)

shapiro.test(res) # no support for deviation from normality
## 
##  Shapiro-Wilk normality test
## 
## data:  res
## W = 0.94294, p-value = 0.2275
mdl_ÖVP_2_scale = lm(data = as.data.frame(scale(df_mdl_ÖVP_2)), ÖVP ~ .)
summary(mdl_ÖVP_2_scale)
## 
## Call:
## lm(formula = ÖVP ~ ., data = as.data.frame(scale(df_mdl_ÖVP_2)))
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.38333 -0.10295 -0.00380  0.06449  0.45318 
## 
## Coefficients:
##                   Estimate Std. Error t value Pr(>|t|)    
## (Intercept)      2.758e-15  4.489e-02   0.000 1.000000    
## age              8.301e-01  6.431e-02  12.908  7.1e-10 ***
## perc_fem         3.999e-01  6.820e-02   5.863  2.4e-05 ***
## net_p_aut       -4.061e-01  9.097e-02  -4.464 0.000391 ***
## net_p_for        5.468e-01  1.325e-01   4.127 0.000791 ***
## local_net_p_for  3.575e-01  1.473e-01   2.427 0.027420 *  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.2106 on 16 degrees of freedom
## Multiple R-squared:  0.9662, Adjusted R-squared:  0.9557 
## F-statistic: 91.53 on 5 and 16 DF,  p-value: 3.424e-11
plot_coefs(mdl_ÖVP_2_scale, colors = "black") + theme_apa() + ggtitle("Beta coefficients for linear regression model, ÖVP results as dependent variable")
## Loading required namespace: broom.mixed

best_mdl_ÖVP = mdl_ÖVP_2

The model of ÖVP has a good fit and is similar to that of FPÖ with regard to migration: Increased Austrian migration is associated with lower support for ÖVP, increased foreign migration is associated with increased support for ÖVP, the size of the effect is however smaller. In the case of ÖVP, age and gender are significant variables, where the older the average population and the larger the percent of female residents, the more support for ÖVP.

SPÖ

df_mdl_SPÖ = subset(mdl_df, select = -c(ÖVP, GRÜNE, FPÖ, NEOS))

mdl_SPÖ = lm(data = df_mdl_SPÖ, SPÖ ~ .)
summary(mdl_SPÖ)
## 
## Call:
## lm(formula = SPÖ ~ ., data = df_mdl_SPÖ)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -1.7030 -0.6481  0.1453  0.7128  1.7223 
## 
## Coefficients:
##                      Estimate Std. Error t value Pr(>|t|)   
## (Intercept)          129.9974    33.5063   3.880  0.00219 **
## age                   -0.9037     0.5117  -1.766  0.10275   
## perc_fem              -0.9418     0.5526  -1.704  0.11406   
## income                -0.3828     0.2846  -1.345  0.20347   
## net_p_aut             14.1291     6.3834   2.213  0.04699 * 
## local_net_p_aut        3.3796     3.4809   0.971  0.35076   
## local_internal_p_aut -19.7894    16.4734  -1.201  0.25281   
## net_p_for             -1.0783     4.8007  -0.225  0.82606   
## local_net_p_for        3.5115     3.6731   0.956  0.35794   
## local_internal_p_for  16.4002    20.9016   0.785  0.44788   
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.255 on 12 degrees of freedom
## Multiple R-squared:  0.9505, Adjusted R-squared:  0.9134 
## F-statistic:  25.6 on 9 and 12 DF,  p-value: 1.713e-06
rsq(mdl_SPÖ) 
## [1] 0.9504881
AIC(mdl_SPÖ) 
## [1] 81.10057
RMSE(mdl_SPÖ) 
## [1] 0.9270537
vif(mdl_SPÖ) 
##                  age             perc_fem               income 
##            11.856388             6.238994            16.930084 
##            net_p_aut      local_net_p_aut local_internal_p_aut 
##            13.638026            15.032455             9.393337 
##            net_p_for      local_net_p_for local_internal_p_for 
##            14.428220            17.739515            10.131135
res = resid(mdl_SPÖ)
qqnorm(res)
qqline(res)

shapiro.test(res) # no support for deviation from normality
## 
##  Shapiro-Wilk normality test
## 
## data:  res
## W = 0.96812, p-value = 0.6675
df_mdl_SPÖ_2 = subset(df_mdl_SPÖ,
                      select = -c(income, local_net_p_for, local_internal_p_for))

mdl_SPÖ_2 = lm(data = df_mdl_SPÖ_2, SPÖ ~ .)
summary(mdl_SPÖ_2)
## 
## Call:
## lm(formula = SPÖ ~ ., data = df_mdl_SPÖ_2)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -2.35145 -0.63241  0.00802  0.88703  1.54004 
## 
## Coefficients:
##                      Estimate Std. Error t value Pr(>|t|)    
## (Intercept)          181.6354    17.1004  10.622 2.25e-08 ***
## age                   -1.5765     0.2563  -6.151 1.85e-05 ***
## perc_fem              -1.5838     0.4434  -3.572  0.00278 ** 
## net_p_aut              7.3028     4.7264   1.545  0.14315    
## local_net_p_aut        0.9959     2.8053   0.355  0.72752    
## local_internal_p_aut -11.3156    10.4516  -1.083  0.29606    
## net_p_for             -3.1982     2.6648  -1.200  0.24869    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.283 on 15 degrees of freedom
## Multiple R-squared:  0.9353, Adjusted R-squared:  0.9095 
## F-statistic: 36.16 on 6 and 15 DF,  p-value: 4.316e-08
rsq(mdl_SPÖ_2) 
## [1] 0.9353367
AIC(mdl_SPÖ_2)
## [1] 80.97418
RMSE(mdl_SPÖ_2) 
## [1] 1.059447
vif(mdl_SPÖ_2) # no VIF > 10
##                  age             perc_fem            net_p_aut 
##             2.847055             3.843767             7.155921 
##      local_net_p_aut local_internal_p_aut            net_p_for 
##             9.344554             3.618923             4.254970
res = resid(mdl_SPÖ_2)
qqnorm(res)
qqline(res)

shapiro.test(res) # no support for deviation from normality
## 
##  Shapiro-Wilk normality test
## 
## data:  res
## W = 0.9623, p-value = 0.5371
mdl_SPÖ_2_scale = lm(data = as.data.frame(scale(df_mdl_SPÖ_2)), SPÖ ~ .)
summary(mdl_SPÖ_2_scale)
## 
## Call:
## lm(formula = SPÖ ~ ., data = as.data.frame(scale(df_mdl_SPÖ_2)))
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.55142 -0.14830  0.00188  0.20801  0.36114 
## 
## Coefficients:
##                        Estimate Std. Error t value Pr(>|t|)    
## (Intercept)          -2.162e-15  6.415e-02   0.000  1.00000    
## age                  -6.815e-01  1.108e-01  -6.151 1.85e-05 ***
## perc_fem             -4.598e-01  1.287e-01  -3.572  0.00278 ** 
## net_p_aut             2.714e-01  1.756e-01   1.545  0.14315    
## local_net_p_aut       7.125e-02  2.007e-01   0.355  0.72752    
## local_internal_p_aut -1.352e-01  1.249e-01  -1.083  0.29606    
## net_p_for            -1.625e-01  1.354e-01  -1.200  0.24869    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.3009 on 15 degrees of freedom
## Multiple R-squared:  0.9353, Adjusted R-squared:  0.9095 
## F-statistic: 36.16 on 6 and 15 DF,  p-value: 4.316e-08
plot_coefs(mdl_SPÖ_2_scale, colors = "red") + theme_apa() + ggtitle("Beta coefficients for linear regression model, SPÖ results as dependent variable")
## Loading required namespace: broom.mixed

best_mdl_SPÖ = mdl_SPÖ_2

For SPÖ, no significant association between migration and party support is found when accounting for other demographic indicators. Instead, an increase in age and percentage of females is associated with lower support for SPÖ.

NEOS

df_mdl_NEOS = subset(mdl_df, select = -c(ÖVP, GRÜNE, FPÖ, SPÖ))

mdl_NEOS = lm(data = df_mdl_NEOS, NEOS ~ .)
summary(mdl_NEOS)
## 
## Call:
## lm(formula = NEOS ~ ., data = df_mdl_NEOS)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1.14283 -0.57189 -0.06258  0.60264  1.25028 
## 
## Coefficients:
##                      Estimate Std. Error t value Pr(>|t|)  
## (Intercept)          -54.8978    26.1362  -2.100   0.0575 .
## age                    0.1946     0.3991   0.488   0.6347  
## perc_fem               0.9475     0.4311   2.198   0.0483 *
## income                 0.4005     0.2220   1.804   0.0963 .
## net_p_aut              3.7944     4.9793   0.762   0.4608  
## local_net_p_aut       -1.9303     2.7152  -0.711   0.4907  
## local_internal_p_aut   5.7904    12.8499   0.451   0.6603  
## net_p_for             -2.1540     3.7447  -0.575   0.5758  
## local_net_p_for       -3.9712     2.8652  -1.386   0.1910  
## local_internal_p_for  -0.5123    16.3041  -0.031   0.9754  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.9791 on 12 degrees of freedom
## Multiple R-squared:  0.9605, Adjusted R-squared:  0.9309 
## F-statistic: 32.45 on 9 and 12 DF,  p-value: 4.537e-07
rsq(mdl_NEOS) #0.96
## [1] 0.9605293
AIC(mdl_NEOS) #70
## [1] 70.17048
RMSE(mdl_NEOS) #0.72
## [1] 0.7231382
vif(mdl_NEOS) #
##                  age             perc_fem               income 
##            11.856388             6.238994            16.930084 
##            net_p_aut      local_net_p_aut local_internal_p_aut 
##            13.638026            15.032455             9.393337 
##            net_p_for      local_net_p_for local_internal_p_for 
##            14.428220            17.739515            10.131135
res = resid(mdl_NEOS)
qqnorm(res)
qqline(res)

shapiro.test(res) # no support for deviation from normality
## 
##  Shapiro-Wilk normality test
## 
## data:  res
## W = 0.95895, p-value = 0.4684
df_mdl_NEOS_2 = subset(
  df_mdl_NEOS,
  select = -c(
    age,
    local_net_p_aut,
    local_internal_p_aut,
    local_internal_p_for
  )
)

mdl_NEOS_2 = lm(data = df_mdl_NEOS_2, NEOS ~ .)
summary(mdl_NEOS_2)
## 
## Call:
## lm(formula = NEOS ~ ., data = df_mdl_NEOS_2)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.93756 -0.71938 -0.06721  0.72112  1.22473 
## 
## Coefficients:
##                  Estimate Std. Error t value Pr(>|t|)    
## (Intercept)     -41.49547   14.43990  -2.874  0.01103 *  
## perc_fem          0.85017    0.29763   2.857  0.01143 *  
## income            0.47958    0.08585   5.586 4.09e-05 ***
## net_p_aut         4.59651    2.39433   1.920  0.07290 .  
## net_p_for        -3.59858    2.46171  -1.462  0.16315    
## local_net_p_for  -5.55879    1.81928  -3.055  0.00755 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.8884 on 16 degrees of freedom
## Multiple R-squared:  0.9567, Adjusted R-squared:  0.9431 
## F-statistic: 70.65 on 5 and 16 DF,  p-value: 2.476e-10
rsq(mdl_NEOS_2) 
## [1] 0.9566712
AIC(mdl_NEOS_2) 
## [1] 64.22221
RMSE(mdl_NEOS_2) 
## [1] 0.7576569
vif(mdl_NEOS_2) 
##        perc_fem          income       net_p_aut       net_p_for local_net_p_for 
##        3.612733        3.075519        3.830217        7.573179        8.687109
res = resid(mdl_NEOS_2)
qqnorm(res)
qqline(res)

shapiro.test(res) # no support for deviation from normality
## 
##  Shapiro-Wilk normality test
## 
## data:  res
## W = 0.89561, p-value = 0.02431
mdl_NEOS_2_scale = lm(data = as.data.frame(scale(df_mdl_NEOS_2)), NEOS ~ .)
summary(mdl_NEOS_2_scale)
## 
## Call:
## lm(formula = NEOS ~ ., data = as.data.frame(scale(df_mdl_NEOS_2)))
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.25166 -0.19309 -0.01804  0.19356  0.32874 
## 
## Coefficients:
##                   Estimate Std. Error t value Pr(>|t|)    
## (Intercept)      7.507e-16  5.084e-02   0.000  1.00000    
## perc_fem         2.825e-01  9.891e-02   2.857  0.01143 *  
## income           5.098e-01  9.126e-02   5.586 4.09e-05 ***
## net_p_aut        1.955e-01  1.018e-01   1.920  0.07290 .  
## net_p_for       -2.093e-01  1.432e-01  -1.462  0.16315    
## local_net_p_for -4.686e-01  1.534e-01  -3.055  0.00755 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.2385 on 16 degrees of freedom
## Multiple R-squared:  0.9567, Adjusted R-squared:  0.9431 
## F-statistic: 70.65 on 5 and 16 DF,  p-value: 2.476e-10
plot_coefs(mdl_NEOS_2_scale, colors = "pink") + theme_apa() + ggtitle("Beta coefficients for linear regression model, NEOS results as dependent variable")
## Loading required namespace: broom.mixed

best_mdl_NEOS = mdl_NEOS_2

In the case of the NEOS, neither external nor internal migration has a significant effect, instead, gender and income and gender have the largest effect. An increase in percentage of female residents and increase in average income is associated with increased support for NEOS. An increase in foreign immigrants within Vienna is also associated with lower support for NEOS.

##Summary

plot_summs(
  best_mdl_FPÖ,
  best_mdl_GRN,
  best_mdl_ÖVP,
  best_mdl_SPÖ,
  best_mdl_NEOS,
  colors = c("blue", 'green', 'black', 'red', 'pink'),
  model.names = c("FPÖ", 'GRÜNE', 'ÖVP', "SPÖ", 'NEOS')
) +
  theme_apa() + ggtitle("Comparison of beta coefficients for modelling party support in Vienna")
## Loading required namespace: broom.mixed
## Loading required namespace: broom.mixed
## Loading required namespace: broom.mixed
## Loading required namespace: broom.mixed
## Loading required namespace: broom.mixed

metrics = data.frame(
  Model = c("FPÖ", "GRÜNE", "ÖVP", "SPÖ", "NEOS"),
  R2 = round(c(summary(best_mdl_FPÖ)$r.squared,
         summary(best_mdl_GRN)$r.squared,
         summary(best_mdl_ÖVP)$r.squared,
         summary(best_mdl_SPÖ)$r.squared,
         summary(best_mdl_NEOS)$r.squared),3),
  AIC = round(c(AIC(best_mdl_FPÖ),
          AIC(best_mdl_GRN),
          AIC(best_mdl_ÖVP),
          AIC(best_mdl_SPÖ),
          AIC(best_mdl_NEOS)),3),
  RMSE = round(c(RMSE(best_mdl_FPÖ),
          RMSE(best_mdl_GRN),
          RMSE(best_mdl_ÖVP),
          RMSE(best_mdl_SPÖ),
          RMSE(best_mdl_NEOS)),3)
)

gt(metrics)
Model R2 AIC RMSE
FPÖ 0.943 96.423 1.505
GRÜNE 0.947 79.083 1.062
ÖVP 0.966 69.696 0.858
SPÖ 0.935 80.974 1.059
NEOS 0.957 64.222 0.758

Conclusion

All in all, several models could successfully be created explaining most of the variability in election results between the Viennese districts. The best model was the one of ÖVP, with an R2 of 0.966 and a Root mean squared error of 0.180. Across the board, recent migration is a significant predictor of election results for all parties except for SPÖ, whose results are explained by a combination of age and gender.

A more positive net migration of persons with foreign citizen ship is associated with a better result for ÖVP and FPÖ, while a more positive net migration of Austrians is associated with a worse result for ÖVP and FPÖ. In the case of ÖVP, the sociodemographic factors age and gender are significant predictors as well. In the case of ÖVP, age is the most important variable, but for FPÖ it is within Vienna migration of foreigners.

The modell of the greens results tell a smiliar story, but their results are positively associated with net migration of austrians and negatively associated with migration of foreigners. Interestingly, income is here significant predictor with a negative direction, indicating that the greens enjoy support from voters with lower income, which could be due to e.g. Students preferring this party.

Just like for the greens, the NEOS has income as a significant predictor, being associated with a larger income however. Here, the effect of income is even the largest and most important, while migration does not have a perticular impact.